#include <iostream>
#include <fstream>
#include <string>
#include <cerrno>
#include <stdlib.h>
#include <GL/glew.h>
#include <GL/freeglut.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#ifndef BUFFER_OFFSET	
#define BUFFER_OFFSET(i) ((char *)NULL + (i))
#endif

//해상도 800*600
#define RES_X 800
#define RES_Y 600

class Axes
{
public:
	GLuint vao, buf_vtx;
	static GLfloat vtx[6][6];

	void initOpenGLAttrib()
	{
		glGenVertexArrays(1, &vao);
		glBindVertexArray(vao);
		glGenBuffers(1, &buf_vtx);
		glBindBuffer(GL_ARRAY_BUFFER, buf_vtx);
		glBufferData(GL_ARRAY_BUFFER, sizeof(vtx), vtx, GL_STATIC_DRAW);
		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), BUFFER_OFFSET(0));
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), BUFFER_OFFSET(3*sizeof(GLfloat)));
	}
};

GLfloat	Axes::vtx[6][6] = {
	{ 0, 0, 0, 1, 0, 0},
	{ 2, 0, 0, 1, 0, 0},
	{ 0, 0, 0, 0, 1, 0},
	{ 0, 2, 0, 0, 1, 0},
	{ 0, 0, 0, 0, 0, 1},
	{ 0, 0, 2, 0, 0, 1},
};


class Desk
{
public:
	GLuint vao[4], buf_vtx[4], buf_idx;
	static GLfloat vtx[4][8][6];
	static GLubyte idx[36];
	glm::mat4 t[4];

	Desk() {
		t[0] = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, 0.0f));
		t[1] = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.5f, -1.0f));
		t[2] = glm::translate(glm::mat4(1.0f), glm::vec3(1.33f, 0.5f, 0.0f));
		t[3] = glm::translate(glm::mat4(1.0f), glm::vec3(-1.33f, 0.5f, 0.0f));
	}

	void initOpenGLAttrib()
	{
		glGenVertexArrays(4, vao);
		glGenBuffers(4, buf_vtx);
		glGenBuffers(1, &buf_idx);

		for(int i = 0;i<4;i++)
		{
			glBindVertexArray(vao[i]);

			glBindBuffer(GL_ARRAY_BUFFER, buf_vtx[i]);
			glBufferData(GL_ARRAY_BUFFER, sizeof(vtx[i]), vtx[i], GL_STATIC_DRAW);

			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf_idx);
			glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

			glEnableVertexAttribArray(0);
			glEnableVertexAttribArray(1);
			glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), BUFFER_OFFSET(0));
			glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), BUFFER_OFFSET(3*sizeof(GLfloat)));
		}
	}
};

GLfloat Desk::vtx[4][8][6] = 	
{
	{
		{-1.33, -0.05, -1, 0.25, 0.2, 0.15},		//0
		{-1.33, -0.05,  1, 0.4, 0.32, 0.24},		//1
		{-1.33,  0.05, -1, 0.40, 0.32, 0.24},		//2
		{-1.33,  0.05,  1, 0.75, 0.64, 0.48},		//3
		{ 1.33, -0.05, -1, 0.3, 0.24, 0.18},		//4
		{ 1.33, -0.05,  1, 0.5, 0.4, 0.3},			//5	
		{ 1.33,  0.05, -1, 0.61, 0.52, 0.40},		//6
		{ 1.33,  0.05,  1, 0.95, 0.84, 0.64}		//7
	},
	{
		{-1.33, -0.5, -0.05, 0.25, 0.2, 0.15},		//0
		{-1.33, -0.5,  0.05, 0.4, 0.32, 0.24},		//1
		{-1.33,  0.5, -0.05, 0.40, 0.32, 0.24},		//2
		{-1.33,  0.5,  0.05, 0.75, 0.64, 0.48},		//3
		{ 1.33, -0.5, -0.05, 0.3, 0.24, 0.18},		//4
		{ 1.33, -0.5,  0.05, 0.5, 0.4, 0.3},		//5	
		{ 1.33,  0.5, -0.05, 0.61, 0.52, 0.40},		//6
		{ 1.33,  0.5,  0.05, 0.95, 0.84, 0.64}		//7
	},
	{
		{-0.05, -0.5, -1, 0.25, 0.2, 0.15},			//0
		{-0.05, -0.5,  1, 0.4, 0.32, 0.24},			//1
		{-0.05,  0.5, -1, 0.40, 0.32, 0.24},		//2
		{-0.05,  0.5,  1, 0.75, 0.64, 0.48},		//3
		{ 0.05, -0.5, -1, 0.3, 0.24, 0.18},			//4
		{ 0.05, -0.5,  1, 0.5, 0.4, 0.3},			//5	
		{ 0.05,  0.5, -1, 0.61, 0.52, 0.40},		//6
		{ 0.05,  0.5,  1, 0.95, 0.84, 0.64}			//7
	},
	{
		{-0.05, -0.5, -1, 0.25, 0.2, 0.15},			//0
		{-0.05, -0.5,  1, 0.4, 0.32, 0.24},			//1
		{-0.05,  0.5, -1, 0.40, 0.32, 0.24},		//2
		{-0.05,  0.5,  1, 0.75, 0.64, 0.48},		//3
		{ 0.05, -0.5, -1, 0.3, 0.24, 0.18},			//4
		{ 0.05, -0.5,  1, 0.5, 0.4, 0.3},			//5	
		{ 0.05,  0.5, -1, 0.61, 0.52, 0.40},		//6
		{ 0.05,  0.5,  1, 0.95, 0.84, 0.64}			//7
	}
};

GLubyte Desk::idx[36] = {
	0, 4, 1,
	1, 4, 5,
	1, 5, 7,
	1, 7, 3,
	0, 1, 3,
	0, 3, 2,
	0, 6, 4,
	0, 2, 6,
	4, 7, 5,
	4, 6, 7,
	2, 3, 7,
	2, 7, 6
};


class Icon
{
public:
	GLuint id;
	GLuint	vao, buf_vtx, buf_idx;
	GLfloat vtx[8][6];
	GLubyte idx[24];
	GLfloat x, y;

	Icon(GLuint id_)
	{
		id = id_;
		vtx[0][0] = -0.05;  vtx[0][1] = -0.025; 	vtx[0][2] = -0.05; 	vtx[0][3] = 1; 	vtx[0][4] = 1; 	vtx[0][5] = 1;
		vtx[1][0] = -0.05;  vtx[1][1] = -0.025; 	vtx[1][2] =  0.05; 	vtx[1][3] = 1; 	vtx[1][4] = 1; 	vtx[1][5] = 1;
		vtx[2][0] = -0.05;  vtx[2][1] =  0.025; 	vtx[2][2] = -0.05; 	vtx[2][3] = 1; 	vtx[2][4] = 1; 	vtx[2][5] = 1;
		vtx[3][0] = -0.05;  vtx[3][1] =  0.025; 	vtx[3][2] =  0.05; 	vtx[3][3] = 1; 	vtx[3][4] = 1; 	vtx[3][5] = 1;
		vtx[4][0] =  0.05;  vtx[4][1] = -0.025; 	vtx[4][2] = -0.05; 	vtx[4][3] = 1; 	vtx[4][4] = 1; 	vtx[4][5] = 1;
		vtx[5][0] =  0.05;  vtx[5][1] = -0.025; 	vtx[5][2] =  0.05; 	vtx[5][3] = 1; 	vtx[5][4] = 1; 	vtx[5][5] = 1;
		vtx[6][0] =  0.05;  vtx[6][1] =  0.025; 	vtx[6][2] = -0.05; 	vtx[6][3] = 1; 	vtx[6][4] = 1; 	vtx[6][5] = 1;
		vtx[7][0] =  0.05;  vtx[7][1] =  0.025; 	vtx[7][2] =  0.05; 	vtx[7][3] = 1; 	vtx[7][4] = 1; 	vtx[7][5] = 1;	
		
		idx[0] = 0; 	idx[1] = 1;
		idx[2] = 0; 	idx[3] = 2;
		idx[4] = 0; 	idx[5] = 4;
		idx[6] = 7; 	idx[7] = 6;
		idx[8] = 7; 	idx[9] = 5;
		idx[10] = 7; 	idx[11] = 3;
		idx[12] = 1; 	idx[13] = 3;
		idx[14] = 3; 	idx[15] = 2;
		idx[16] = 2; 	idx[17] = 6;
		idx[18] = 6; 	idx[19] = 4;
		idx[20] = 4; 	idx[21] = 5;
		idx[22] = 5; 	idx[23] = 1;
	}

	void initOpenGLAttrib() {
		glGenVertexArrays(1, &vao);
		glBindVertexArray(vao);

		glGenBuffers(1, &buf_vtx);
		glBindBuffer(GL_ARRAY_BUFFER, buf_vtx);
		glBufferData(GL_ARRAY_BUFFER, sizeof(vtx), vtx, GL_STATIC_DRAW);

		glGenBuffers(1, &buf_idx);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf_idx);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), BUFFER_OFFSET(0));
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), BUFFER_OFFSET(3*sizeof(GLfloat)));
	}
};

class Node
{
public:
	Node* next;
	Icon* data;

	Node(Icon* icon)
	{
		data = icon;
		next = NULL;
	}
};

class LinkedList
{
public:
	Node* head;

	LinkedList()
	{
		head = NULL;
	}

	void add(Icon* icon)
	{
		Node* n = new Node(icon);
		if(head == NULL)
		{
			head = n;
		}
		else
		{
			n->next = head->next;
			head->next = n;
		}
	}

	void deleteByID(GLuint id)
	{
		Node* ptr = head;
		Node* preNode = ptr;
		for(;ptr != NULL; ptr = ptr->next)
		{
			if(ptr->data->id == id)
			{
				if(ptr == head)
				{
					ptr->data = NULL;
					head = ptr->next;
					break;
				}
				else
				{
					ptr->data = NULL;
					preNode->next = ptr->next;
					ptr->next = NULL;
					break;
				}
			}
			preNode = ptr;
		}
	}

	Icon* selectByID(GLuint id)
	{
		Node* ptr = head;
		for(;ptr != NULL; ptr = ptr->next)
		{
			if(ptr->data->id == id)
			{
				return ptr->data;
			}
		}
		return NULL;
	}
};

GLuint h_prog;
GLuint loc_MVP;
GLuint numOfIcon = 0;
GLfloat	angleY = 0, angleX = 60.0f; // 책상이 X, Y축으로 회전되는 각도.
GLint mouseX, mouseY; // 현재 화면상의 마우스 좌표, 항상 갱신된다.
Axes axes;
Desk desk;
LinkedList ll;


glm::mat4	p = glm::perspective(45.0f, 1.33f, 1.0f, 10.0f);
glm::mat4	rY = glm::rotate(glm::mat4(1.0f), angleY, glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4	rX = glm::rotate(glm::mat4(1.0f), angleX, glm::vec3(1.0f, 0.0f, 0.0f));
glm::mat4	t = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, -0.2f, -3.0f));

bool check_compile_status(GLuint handle)
{
	GLint	status;
	glGetShaderiv(handle, GL_COMPILE_STATUS, &status);
	if(status == GL_FALSE)
	{
		GLint	len;
		glGetShaderiv(handle, GL_INFO_LOG_LENGTH, &len);
		GLchar  *buf_log = new GLchar [len+1];
		glGetShaderInfoLog(handle, len, NULL, buf_log);
		std::cerr << "Compilation failed: " << buf_log << std::endl;
		delete[] buf_log;
	}
	return (status==GL_TRUE);
}

GLuint compile_shader(GLenum type, const char* source)
{
	GLuint	handle = glCreateShader(type);
	if(!handle)
	{
		std::cerr << "Shader object creation failed." << std::endl;
		return handle;
	}
	glShaderSource( handle, 1, &source, NULL );
	glCompileShader( handle );
	if(!check_compile_status(handle))
	{
		std::cerr << "Shader compilation failed." << std::endl;
		return 0;
	}
	return handle;
}

bool check_link_status(GLuint handle)
{
	GLint	status;
	glGetProgramiv(handle, GL_LINK_STATUS, &status);
	if(status == GL_FALSE)
	{
		GLint	len;
		glGetProgramiv(handle, GL_INFO_LOG_LENGTH, &len);
		GLchar	*buf_log = new GLchar[len+1];
		glGetProgramInfoLog(handle, len, NULL, buf_log);
		std::cerr << "Linking failed: " << buf_log << std::endl;
		delete[] buf_log;
	}
	return (status==GL_TRUE);
}

GLuint build_program(const char* src_vert, const char* src_frag)
{
	GLuint	h_vert = compile_shader(GL_VERTEX_SHADER, src_vert);
	if(!h_vert)
	{
		std::cerr << "Error while compiling the vertex shader" << std::endl;
		return 0;
	}
	GLuint	h_frag = compile_shader(GL_FRAGMENT_SHADER, src_frag);
	if(!h_frag)
	{
		std::cerr << "Error wihle compiling the fragment shader" << std::endl;
		return 0;
	}

	GLuint h_prog = glCreateProgram();
	if(!h_prog)
	{
		std::cerr << "Program object creation failed." << std::endl;
		return h_prog;
	}
	glAttachShader( h_prog, h_vert );
	glAttachShader( h_prog, h_frag );
	glLinkProgram( h_prog );
	if(!check_link_status(h_prog))	return 0;

	return h_prog;
}

// original source:
// http://stackoverflow.com/questions/2602013/read-whole-ascii-file-into-c-stdstring
//
std::string get_file_contents(const char *filename)
{
  std::ifstream in(filename, std::ios::in | std::ios::binary);
  if (in)
  {
    std::string contents;
    in.seekg(0, std::ios::end);
    contents.resize(in.tellg());
    in.seekg(0, std::ios::beg);
    in.read(&contents[0], contents.size());
    in.close();
    return(contents);
  }
  throw(errno);
}

GLuint build_program_from_files(const char* file_vert, const char* file_frag)
{
	std::string src_vert = get_file_contents(file_vert);
	std::string src_frag = get_file_contents(file_frag);
	return build_program(src_vert.c_str(), src_frag.c_str());
}

// 클릭된 마우스 좌표를 3D 공간의 좌표로 바꿔준다. 가장 가까이 있는 객체와의 교점의 좌표로 바뀐다.
glm::vec3 convertMouseCoordToWorldCoord(GLfloat x, GLfloat y)
{
	GLint viewport[4];
	glm::vec3 wPos;

	glGetIntegerv( GL_VIEWPORT, viewport );
	wPos[0] = x;
	wPos[1] = viewport[3] - y;

	glReadPixels(x, wPos[1], 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &wPos[2]);
	
	wPos = glm::unProject(wPos,t*rX,p,glm::vec4(0,0,800,600));

	return wPos;
}

// 아이콘을 생성한다. x, y는 마우스 좌표계의 좌표이다.
void createIcon(int x, int y)
{
	Icon* icon = new Icon(numOfIcon);

	glm::vec3 worldPos = convertMouseCoordToWorldCoord((float)x, (float)y);  
    icon->x = worldPos[0];
    icon->y = worldPos[2];
	
	icon->initOpenGLAttrib();

	ll.add(icon);
	numOfIcon++;
}

void init(void)
{
	GLint view[4];
	glGetIntegerv(GL_VIEWPORT, view);

	desk.initOpenGLAttrib();

	axes.initOpenGLAttrib();

	h_prog = build_program_from_files("3DDesktop.vert", "3DDesktop.frag");

	loc_MVP = glGetUniformLocation(h_prog, "MVP");

	glUseProgram(h_prog);

	glEnable(GL_DEPTH_TEST);
}

void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    rY = glm::rotate(glm::mat4(1.0f), angleY, glm::vec3(0.0f, 1.0f, 0.0f));
    rX = glm::rotate(glm::mat4(1.0f), angleX, glm::vec3(1.0f, 0.0f, 0.0f));

	glm::mat4 translateOnDesk = p*t*rX*rY; // 책상에 가해지는 트랜슬레이트 매트릭스. 다른 객체를 트랜슬레이션하려면 책상과의 상대적인 트랜슬레이션을 먼저 한 후에 마지막에 곱하면 된다.

	glLineWidth(2.0f);

	// 책상.
	for(int i=0;i<4;i++)
	{
		glBindVertexArray(desk.vao[i]);
		glUniformMatrix4fv(loc_MVP, 1, GL_FALSE, glm::value_ptr(translateOnDesk*desk.t[i]));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, BUFFER_OFFSET(0));
	}

	// 아이콘.
	glm::mat4	T_each = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, 0.0f));
	for(Node* n = ll.head;n != NULL; n = n->next)
	{
		glBindVertexArray(n->data->vao);
		glLineWidth(2.0f);
		T_each = glm::translate(glm::mat4(1.0f), glm::vec3(n->data->x, 0.075f, n->data->y));
		glUniformMatrix4fv(loc_MVP, 1, GL_FALSE, glm::value_ptr(translateOnDesk*T_each));
		glDrawElements(GL_LINES, 24, GL_UNSIGNED_BYTE, BUFFER_OFFSET(0));
	}

	// 축.
	glLineWidth(1.0f);
	glBindVertexArray(axes.vao);
	//glUniformMatrix4fv(glGetUniformLocation(h_prog, "MVP"), 1, GL_FALSE, glm::value_ptr(translateOnDesk));
	glUniformMatrix4fv(loc_MVP, 1, GL_FALSE, glm::value_ptr(translateOnDesk));
	glDrawArrays(GL_LINES, 0, 6);

	glutSwapBuffers();
}

void idle(void)
{
	glutPostRedisplay();
}

// 각각 마우스와 키보드 입력을 처리하는 함수들.
void mouse(int e, int s, int x, int y)
{
}

// E, Q, W, S는 화면 전환(테스트용), I는 현재 마우스 위치에 아이콘을 생성하는 기능을 한다.
void keyboard(unsigned char key, int x, int y)
{
	switch(key)
	{
	case 'E':
		angleY += 90.0f;
		glutPostRedisplay();
		break;
	case 'e':
		angleY += 90.0f;
		glutPostRedisplay();
		break;
	case 'Q':
		angleY -= 90.0f;
		glutPostRedisplay();
		break;
	case 'q':
		angleY -= 90.0f;
		glutPostRedisplay();
		break;
	case 'W':
		angleX += 30.0f;
		glutPostRedisplay();
		break;
	case 'w':
		angleX += 30.0f;
		glutPostRedisplay();
		break;
	case 'S':
		angleX -= 30.0f;
		glutPostRedisplay();
		break;
	case 's':
		angleX -= 30.0f;
		glutPostRedisplay();
		break;
	case 'I':
		createIcon(mouseX, mouseY);
		glutPostRedisplay();
		break;
	case 'i':
		createIcon(mouseX, mouseY);
		glutPostRedisplay();
		break;
	}
}

// 마우스 우클릭 시 호출되는 메뉴
void rightClick_menu(int value) {
	if(value==1)
	{
		createIcon(mouseX, mouseY);
	}
}

// 이 함수에 의해 마우스 좌표는 글로벌 변수인 mouseX, mouseY에 항상 갱신된다.
void mousePassive(int x, int y)
{
	mouseX = x;
	mouseY = y;
}

int main(int argc, char** argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
	glutInitWindowSize(800, 600);
	glutInitContextVersion(4,0);
	glutInitContextFlags (GLUT_CORE_PROFILE | GLUT_DEBUG);
	glutInitContextProfile(GLUT_FORWARD_COMPATIBLE);
	glutCreateWindow(argv[0]);
	if (glewInit()) {
		std::cerr << "Unable to initialize GLEW ... exiting" << std::endl;
		exit(EXIT_FAILURE);
	}
	init();
	glutCreateMenu(rightClick_menu);
	glutAddMenuEntry("Create icon", 1);
	glutAddMenuEntry("Create object", 2);
	glutAttachMenu(GLUT_RIGHT_BUTTON);
	glutDisplayFunc(display);
	glutMouseFunc(mouse);
	glutPassiveMotionFunc(mousePassive);
	glutKeyboardFunc(keyboard);
	glutIdleFunc(idle);
	glutMainLoop();
}
